home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / glchess / chess / pgn.py < prev    next >
Encoding:
Python Source  |  2009-04-14  |  18.5 KB  |  595 lines

  1. # -*- coding: utf-8 -*-
  2. """
  3. Implement a PGN reader/writer.
  4.  
  5. See http://www.chessclub.com/help/PGN-spec
  6. """
  7.  
  8. __author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
  9. __license__ = 'GNU General Public License Version 2'
  10. __copyright__ = 'Copyright 2005-2006  Robert Ancell'
  11.  
  12. import re
  13.  
  14. """
  15. ; Example PGN file
  16.  
  17. [Event "F/S Return Match"]
  18. [Site "Belgrade, Serbia JUG"]
  19. [Date "1992.11.04"]
  20. [Round "29"]
  21. [White "Fischer, Robert J."]
  22. [Black "Spassky, Boris V."]
  23. [Result "1/2-1/2"]
  24.  
  25. 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3
  26. O-O 9. h3 Nb8 10. d4 Nbd7 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15.
  27. Nb1 h6 16. Bh4 c5 17. dxe5 Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21.
  28. Nc4 Nxc4 22. Bxc4 Nb6 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7
  29. 27. Qe3 Qg5 28. Qxg5 hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33.
  30. f3 Bc8 34. Kf2 Bf5 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5
  31. 40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2
  32. """
  33.  
  34. RESULT_INCOMPLETE = '*'
  35. RESULT_WHITE_WIN  = '1-0'
  36. RESULT_BLACK_WIN  = '0-1'
  37. RESULT_DRAW       = '1/2-1/2'
  38. results = {RESULT_INCOMPLETE: RESULT_INCOMPLETE,
  39.            RESULT_WHITE_WIN: RESULT_WHITE_WIN,
  40.            RESULT_BLACK_WIN: RESULT_BLACK_WIN,
  41.            RESULT_DRAW: RESULT_DRAW}
  42.  
  43. """The required tags in a PGN file (the seven tag roster, STR)"""
  44. TAG_EVENT  = 'Event'
  45. TAG_SITE   = 'Site'
  46. TAG_DATE   = 'Date'
  47. TAG_ROUND  = 'Round'
  48. TAG_WHITE  = 'White'
  49. TAG_BLACK  = 'Black'
  50. TAG_RESULT = 'Result'
  51.  
  52. """Optional tags"""
  53. TAG_TIME         = 'Time'
  54. TAG_FEN          = 'FEN'
  55. TAG_WHITE_TYPE   = 'WhiteType'
  56. TAG_WHITE_ELO    = 'WhiteElo'
  57. TAG_BLACK_TYPE   = 'BlackType'
  58. TAG_BLACK_ELO    = 'BlackElo'
  59. TAG_TIME_CONTROL = 'TimeControl'
  60. TAG_TERMINATION  = 'Termination'
  61.  
  62. # Values for the WhiteType and BlackType tag
  63. PLAYER_HUMAN     = 'human'
  64. PLAYER_AI        = 'program'
  65.  
  66. # Values for the Termination tag
  67. TERMINATE_ABANDONED        = 'abandoned'
  68. TERMINATE_ADJUDICATION     = 'adjudication'
  69. TERMINATE_DEATH            = 'death'
  70. TERMINATE_EMERGENCY        = 'emergency'
  71. TERMINATE_NORMAL           = 'normal'
  72. TERMINATE_RULES_INFRACTION = 'rules infraction'
  73. TERMINATE_TIME_FORFEIT     = 'time forfeit'
  74. TERMINATE_UNTERMINATED     = 'unterminated'
  75.  
  76. # Comments are bounded by ';' to '\n' or '{' to '}'
  77. # Lines starting with '%' are ignored and are used as an extension mechanism
  78. # Strings are bounded by '"' and '"' and quotes inside the strings are escaped with '\"'
  79.  
  80. # Token types
  81. TOKEN_LINE_COMMENT = 'Line comment'
  82. TOKEN_COMMENT      = 'Comment'
  83. TOKEN_ESCAPED      = 'Escaped data'
  84. TOKEN_PERIOD       = 'Period'
  85. TOKEN_TAG_START    = 'Tag start'
  86. TOKEN_TAG_END      = 'Tag end'
  87. TOKEN_STRING       = 'String'
  88. TOKEN_SYMBOL       = 'Symbol'
  89. TOKEN_RAV_START    = 'RAV start'
  90. TOKEN_RAV_END      = 'RAV end'
  91. TOKEN_XML          = 'XML'
  92. TOKEN_NAG          = 'NAG'
  93.  
  94. class Error(Exception):
  95.     """PGN exception class"""
  96.     pass
  97.  
  98. class PGNParser:
  99.     """
  100.     """
  101.     
  102.     STATE_IDLE       = 'IDLE'
  103.     STATE_TAG_NAME   = 'TAG_NAME'
  104.     STATE_TAG_VALUE  = 'TAG_VALUE'
  105.     STATE_TAG_END    = 'TAG_END'
  106.     STATE_MOVETEXT   = 'MOVETEXT'
  107.     STATE_RAV        = 'RAV'
  108.     STATE_XML        = 'XML'
  109.     
  110.     def __init__(self, maxGames = -1):
  111.         expressions = ['\%.*',         # Escaped data
  112.                        ';.*',          # Line comment
  113.                        '\{',           # Comment start
  114.                        '\".*\"',       # String
  115.                        '[a-zA-Z0-9\*\_\+\#\=\:\-\/]+', # Symbol, '/' Not in spec but required from game draw and incomplete
  116.                        '\[',           # Tag start
  117.                        '\]',           # Tag end
  118.                        '\$[0-9]{1,3}', # NAG
  119.                        '\(',           # RAV start
  120.                        '\)',           # RAV end
  121.                        '\<.*\>',       # XML
  122.                        '[.]+']         # Period(s)
  123.         self.regexp = re.compile('|'.join(expressions))
  124.  
  125.         self.tokens = {';':  TOKEN_LINE_COMMENT,
  126.                        '{':  TOKEN_COMMENT,
  127.                        '[':  TOKEN_TAG_START,
  128.                        ']':  TOKEN_TAG_END,
  129.                        '"':  TOKEN_STRING,
  130.                        '.':  TOKEN_PERIOD,
  131.                        '$':  TOKEN_NAG,
  132.                        '(':  TOKEN_RAV_START,
  133.                        ')':  TOKEN_RAV_END,
  134.                        '<':  TOKEN_XML,
  135.                        '%':  TOKEN_ESCAPED}
  136.         for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*':
  137.             self.tokens[c] = TOKEN_SYMBOL
  138.  
  139.         self.games = []
  140.         self.maxGames = maxGames
  141.         self.comment = None
  142.  
  143.         self.state = self.STATE_IDLE
  144.         self.game = PGNGame() # Game being assembled       
  145.         self.tagName = None # The tag being assembled
  146.         self.tagValue = None
  147.         self.prevTokenIsMoveNumber = False
  148.         self.currentMoveNumber = 0    
  149.         self.ravDepth = 0     # The Recursive Annotation Variation (RAV) stack
  150.  
  151.     def _parseTokenMovetext(self, tokenType, data):
  152.         """
  153.         """
  154.         if tokenType is TOKEN_SYMBOL:
  155.             # Ignore tokens inside RAV
  156.             if self.ravDepth != 0:
  157.                 return
  158.  
  159.             # See if this is a game terminate
  160.             if results.has_key(data):
  161.                 self.games.append(self.game)
  162.                 self.game = PGNGame()
  163.                 self.prevTokenIsMoveNumber = False
  164.                 self.currentMoveNumber = 0    
  165.                 self.ravDepth = 0
  166.                 self.state = self.STATE_IDLE
  167.             
  168.             # Otherwise it is a move number or a move
  169.             else:
  170.                 try:
  171.                     moveNumber = int(data)
  172.                 except ValueError:
  173.                     move = PGNMove()
  174.                     move.number = self.currentMoveNumber
  175.                     move.move = data
  176.                     self.game.addMove(move)
  177.                     self.currentMoveNumber += 1
  178.                 else:
  179.                     self.prevTokenIsMoveNumber = True
  180.                     expected = (self.currentMoveNumber / 2) + 1
  181.                     if moveNumber != expected:
  182.                         raise Error('Expected move number %i, got %i' % (expected, moveNumber))
  183.  
  184.         elif tokenType is TOKEN_NAG:
  185.             # Ignore tokens inside RAV
  186.             if self.ravDepth != 0:
  187.                 return
  188.             
  189.             move = self.game.getMove(self.currentMoveNumber)
  190.             move.nag = data
  191.             
  192.         elif tokenType is TOKEN_PERIOD:
  193.             # Ignore tokens inside RAV
  194.             if self.ravDepth != 0:
  195.                 return           
  196.  
  197.             if self.prevTokenIsMoveNumber is False:
  198.                 raise Error('Unexpected period')
  199.  
  200.         elif tokenType is TOKEN_RAV_START:
  201.             self.ravDepth += 1
  202.             # FIXME: Check for RAV errors
  203.             return
  204.                 
  205.         elif tokenType is TOKEN_RAV_END:
  206.             self.ravDepth -= 1
  207.             # FIXME: Check for RAV errors
  208.             return
  209.                
  210.         else:
  211.             raise Error('Unknown token %s in movetext' % (str(tokenType)))
  212.     
  213.     def parseToken(self, tokenType, data):
  214.         """
  215.         """
  216.         # Ignore all comments at any time
  217.         if tokenType is TOKEN_LINE_COMMENT or tokenType is TOKEN_COMMENT:
  218.             if self.currentMoveNumber > 0:
  219.                 move = self.game.getMove(self.currentMoveNumber)
  220.                 move.comment = data[1:-1]
  221.             return
  222.        
  223.         if self.state is self.STATE_MOVETEXT:
  224.             self._parseTokenMovetext(tokenType, data)
  225.             
  226.         elif self.state is self.STATE_IDLE:                
  227.             if tokenType is TOKEN_TAG_START:
  228.                 self.state = self.STATE_TAG_NAME
  229.                 return
  230.  
  231.             elif tokenType is TOKEN_SYMBOL:
  232.                 self.whiteMove = None
  233.                 self.prevTokenIsMoveNumber = False
  234.                 self.ravDepth = 0
  235.                 self.state = self.STATE_MOVETEXT
  236.                 self._parseTokenMovetext(tokenType, data)
  237.                 
  238.             elif tokenType is TOKEN_ESCAPED:
  239.                 pass
  240.  
  241.             else:
  242.                 raise Error('Unexpected token %s' % (str(tokenType)))
  243.  
  244.         if self.state is self.STATE_TAG_NAME:
  245.             if tokenType is TOKEN_SYMBOL:
  246.                 self.tagName = data
  247.                 self.state = self.STATE_TAG_VALUE
  248.             else:
  249.                 raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_SYMBOL)))
  250.  
  251.         elif self.state is self.STATE_TAG_VALUE:
  252.             if tokenType is TOKEN_STRING:
  253.                 self.tagValue = data[1:-1]
  254.                 self.state = self.STATE_TAG_END
  255.             else:
  256.                 raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_STRING)))
  257.  
  258.         elif self.state is self.STATE_TAG_END:
  259.             if tokenType is TOKEN_TAG_END:
  260.                 self.game.setTag(self.tagName, self.tagValue)
  261.                 self.state = self.STATE_IDLE
  262.             else:
  263.                 raise Error('Got a %s token, expecting a %s token' % (repr(tokenType), repr(TOKEN_TAG_END)))
  264.  
  265.     def parseLine(self, line):
  266.         """Parse a line from a PGN file.
  267.         
  268.         Return an array of tokens extracted from the line.
  269.         """
  270.         while len(line) > 0:
  271.             if self.comment is not None:
  272.                 end = line.find('}')
  273.                 if end < 0:
  274.                     self.comment += line
  275.                     return True
  276.                 else:
  277.                     comment = self.comment + line[:end]
  278.                     self.comment = None
  279.                     self.parseToken(TOKEN_COMMENT, comment)
  280.                     line = line[end+1:]
  281.                 continue
  282.             
  283.             for match in self.regexp.finditer(line):
  284.                 text = line[match.start():match.end()]
  285.                 if text == '{':
  286.                     line = line[match.end():]
  287.                     self.comment = ''
  288.                     break
  289.                 else:
  290.                     try:
  291.                         tokenType = self.tokens[text[0]]
  292.                     except KeyError:
  293.                         raise Error("Unknown token %s" % repr(text))                        
  294.                     self.parseToken(tokenType, text)
  295.  
  296.             if self.comment is None:
  297.                 return True
  298.             
  299.     def complete(self):
  300.         if len(self.game.moves) > 0:
  301.             self.games.append(self.game)
  302.    
  303. class PGNMove:
  304.     """
  305.     """
  306.     #
  307.     move       = ''
  308.     
  309.     #
  310.     comment    = ''
  311.  
  312.     #
  313.     nag        = ''
  314.  
  315. class PGNGame:
  316.     """
  317.     """
  318.  
  319.     # The seven tag roster in the required order (REFERENCE)
  320.     _strTags = [TAG_EVENT, TAG_SITE, TAG_DATE, TAG_ROUND, TAG_WHITE, TAG_BLACK, TAG_RESULT]
  321.  
  322.     def __init__(self):
  323.         # Set the default STR tags
  324.         self.tagsByName = {}
  325.         self.setTag(TAG_EVENT, '?')
  326.         self.setTag(TAG_SITE, '?')
  327.         self.setTag(TAG_DATE, '????.??.??')
  328.         self.setTag(TAG_ROUND, '?')
  329.         self.setTag(TAG_WHITE, '?')
  330.         self.setTag(TAG_BLACK, '?')
  331.         self.setTag(TAG_RESULT, '*')
  332.         self.moves = []
  333.         
  334.     def getLines(self):
  335.         lines = []
  336.         
  337.         # Get the names of the non STR tags
  338.         otherTags = list(set(self.tagsByName).difference(self._strTags))
  339.  
  340.         # Write seven tag roster and the additional tags
  341.         for name in self._strTags + otherTags:
  342.             value = self.tagsByName[name]
  343.             lines.append('['+ name + ' ' + self._makePGNString(value) + ']')
  344.  
  345.         lines.append('')
  346.         
  347.         # Insert numbers in-between moves
  348.         tokens = []
  349.         moveNumber = 0
  350.         for m in self.moves:
  351.             if moveNumber % 2 == 0:
  352.                 tokens.append('%i.' % (moveNumber / 2 + 1))
  353.             moveNumber += 1
  354.             tokens.append(m.move)
  355.             if m.nag != '':
  356.                 tokens.append(m.nag)
  357.             if m.comment != '':
  358.                 tokens.append('{' + m.comment + '}')
  359.                 
  360.         # Add result token to the end
  361.         tokens.append(self.tagsByName[TAG_RESULT])
  362.  
  363.         # Print moves keeping the line length to less than 256 characters (PGN requirement)
  364.         line = ''
  365.         for t in tokens:
  366.             if line == '':
  367.                 x = t
  368.             else:
  369.                 x = ' ' + t
  370.             if len(line) + len(x) >= 80: #>= 256:
  371.                 lines.append(line)
  372.                 line = t
  373.             else:
  374.                 line += x
  375.  
  376.         lines.append(line)
  377.         return lines
  378.         
  379.     def setTag(self, name, value):
  380.         """Set a PGN tag.
  381.         
  382.         'name' is the name of the tag to set (string).
  383.         'value' is the value to set the tag to (string) or None to delete the tag.
  384.         
  385.         Tag names cannot contain whitespace.
  386.         
  387.         Deleting a tag that does not exist has no effect.
  388.         
  389.         Deleting a STR tag or setting one to an invalid value will raise an Error exception.
  390.         """
  391.         if self._isValidTagName(name) is False:
  392.             raise Error('%s is an invalid tag name' % str(name))
  393.  
  394.         # If no value delete
  395.         if value is None:
  396.             # If is a STR tag throw an exception
  397.             if self._strTags.has_key(name):
  398.                 raise Error('%s is a PGN STR tag and cannot be deleted' % name)
  399.             
  400.             # Delete the tag
  401.             try:
  402.                 self._strTags.pop(name)
  403.             except KeyError:
  404.                 pass
  405.         
  406.         # Otherwise set the tag to the new value
  407.         else:
  408.             # FIXME: Validate if it is a STR tag
  409.             
  410.             self.tagsByName[name] = value
  411.     
  412.     def getTag(self, name, default = None):
  413.         """Get a PGN tag.
  414.         
  415.         'name' is the name of the tag to get (string).
  416.         'default' is the default value to return if this valid is missing (user-defined).
  417.         
  418.         Return the value of the tag (string) or the default if the tag does not exist.
  419.         """
  420.         try:
  421.             return self.tagsByName[name]
  422.         except KeyError:
  423.             return default
  424.         
  425.     def addMove(self, move):
  426.         self.moves.append(move)
  427.  
  428.     def getMove(self, moveNumber):
  429.         return self.moves[moveNumber - 1]
  430.     
  431.     def getMoves(self):
  432.         return self.moves
  433.  
  434.     def __str__(self):
  435.         string = ''
  436.         for tag, value in self.tagsByName.iteritems():
  437.             string += '%s = %s\n' % (tag, value)
  438.         string += '\n'
  439.         
  440.         number = 1
  441.         moves = self.moves
  442.         while len(moves) >= 2:
  443.             string += '%3i. %s %s\n' % (number, moves[0].move, moves[1].move)
  444.             number += 1
  445.             moves = moves[2:]
  446.         if len(moves) > 0:
  447.             string += '%3i. %s\n' % (number, moves[0].move)
  448.             
  449.         return string
  450.     
  451.     # Private methods    
  452.     def _makePGNString(self, string):
  453.         """Make a PGN string.
  454.         
  455.         'string' is the string to convert to a PGN string (string).
  456.         
  457.         All characters are valid and quotes are escaped with '\"'.
  458.         
  459.         Return the string surrounded with quotes. e.g. 'Mike "Dog" Smith' -> '"Mike \"Dog\" Smith"'
  460.         """
  461.         pgnString = string
  462.         pgnString.replace('"', '\\"')
  463.         return '"' + pgnString + '"'    
  464.  
  465.     def _isValidTagName(self, name):
  466.         """Valid a PGN tag name.
  467.         
  468.         'name' is the tag name to validate (string).
  469.         
  470.         Tags can only contain the characters, a-Z A-Z and _.
  471.         
  472.         Return True if this is a valid tag name otherwise return False.
  473.         """
  474.         if name is None or len(name) == 0:
  475.             return False
  476.  
  477.         validCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
  478.         for c in name:
  479.             if validCharacters.find(c) < 0:
  480.                 return False
  481.         return True
  482.  
  483. class PGN:
  484.     """
  485.     """
  486.     
  487.     __games = None
  488.  
  489.     def __init__(self, fileName = None, maxGames = None):
  490.         """Create a PGN reader/writer.
  491.         
  492.         'fileName' is the file to load the PGN from or None to generate an empty PGN file.
  493.         'maxGames' is the maximum number of games to load from the file or None
  494.                    to load the whole file. (int, Only applicable if a filename is supplied).
  495.         """       
  496.         self.__games = []
  497.  
  498.         if fileName is not None:
  499.             self.__load(fileName, maxGames)
  500.             
  501.     def addGame(self):
  502.         """Add a new game to the PGN file.
  503.         
  504.         Returns the PGNGame instance to modify"""
  505.         game = PGNGame()
  506.         self.__games.append(game)
  507.         return game
  508.     
  509.     def getGame(self, index):
  510.         """Get a game from the PGN file.
  511.         
  512.         'index' is the game index to get (integer, 0-N).
  513.         
  514.         Return this PGN game or raise an IndexError if no game with this index.
  515.         """
  516.         return self.__games[index]
  517.     
  518.     def save(self, fileName):
  519.         """Save the PGN file.
  520.         
  521.         'fileName' is the name of the file to save to.
  522.         """
  523.         f = file(fileName, 'w')
  524.         # FIXME: Set the newline characters to the correct type?
  525.         
  526.         # Sign it from glChess
  527.         f.write('; PGN saved game generated by glChess\n')
  528.         f.write('; http://glchess.sourceforge.net\n')
  529.  
  530.         for game in self.__games:
  531.             f.write('\n')
  532.             for line in game.getLines():
  533.                 f.write(line + '\n')
  534.             
  535.         f.close()
  536.         
  537.     def __len__(self):
  538.         return len(self.__games)
  539.         
  540.     def __getitem__(self, index):
  541.         return self.__games[index]
  542.  
  543.     def __getslice__(self, start, end):
  544.         return self.__games[start:end]
  545.     
  546.     # Private methods
  547.  
  548.     def __load(self, fileName, maxGames = None):
  549.         """
  550.         """
  551.         # Convert the file into PGN tokens
  552.         f = file(fileName, 'r')
  553.         p = PGNParser(maxGames)
  554.         lineNumber = 0
  555.         try:
  556.             for line in f.readlines():
  557.                 lineNumber += 1                
  558.                 p.parseLine(line)
  559.             p.complete()
  560.         except Error, e:
  561.             raise Error('Error on line %d: %s' % (lineNumber, e.message))
  562.  
  563.         # Must be at least one game in the PGN file
  564.         self.__games = p.games
  565.         if len(self.__games) == 0:
  566.             raise Error('Empty PGN file')
  567.  
  568.         # Tidy up
  569.         f.close()
  570.  
  571. if __name__ == '__main__':
  572.     import time
  573.  
  574.     def test(fileName, maxGames = None):
  575.         s = time.time()
  576.         p = PGN(fileName, maxGames)
  577.         print time.time() - s
  578.         number = 1
  579.         games = p[:]
  580.         #for game in games:
  581.         #    print 'Game ' + str(number)
  582.         #    print game
  583.         #    print 
  584.         #    number += 1
  585.  
  586.     #test('example.pgn')
  587.     #test('rav.pgn')
  588.     #test('wolga-benko.pgn', 3)
  589.     
  590.     #test('wolga-benko.pgn')
  591.     #test('yahoo_chess.pgn')
  592.  
  593.     #p = PGN('example.pgn')
  594.     #p.save('out.pgn')
  595.